/* global define, Promise */
(function (root, factory) {
  'use strict';
  if (typeof module === 'object' && module.exports && typeof require === 'function') {
      // CommonJS
      module.exports = factory();
  } else if (typeof define === 'function' && typeof define.amd === 'object') {
      // AMD. Register as an anonymous module.
      define(factory);
  } else {
      // Browser globals
      root.Queue = factory();
  }
})
(this, function () {
  'use strict';

  /**
   * @return {Object}
   */
  var LocalPromise = typeof Promise !== 'undefined' ? Promise : function () {
      return {
          then: function () {
              throw new Error('Queue.configure() before use Queue');
          }
      };
  };

  var noop = function () {};

  /**
   * @param {*} value
   * @returns {LocalPromise}
   */
  var resolveWith = function (value) {
      if (value && typeof value.then === 'function') {
          return value;
      }

      return new LocalPromise(function (resolve) {
          resolve(value);
      });
  };

  /**
   * It limits concurrently executed promises
   *
   * @param {Number} [maxPendingPromises=Infinity] max number of concurrently executed promises
   * @param {Number} [maxQueuedPromises=Infinity]  max number of queued promises
   * @constructor
   *
   * @example
   *
   * var queue = new Queue(1);
   *
   * queue.push(function () {
   *     // resolve of this promise will resume next request
   *     return downloadTarballFromGithub(url, file);
   * })
   * .then(function (file) {
   *     doStuffWith(file);
   * });
   *
   * queue.push(function () {
   *     return downloadTarballFromGithub(url, file);
   * })
   * // This request will be paused
   * .then(function (file) {
   *     doStuffWith(file);
   * });
   */
  function Queue(maxPendingPromises, maxQueuedPromises) {
      this.pendingPromises = 0;
      this.maxPendingPromises = typeof maxPendingPromises !== 'undefined' ? maxPendingPromises : Infinity;
      this.maxQueuedPromises = typeof maxQueuedPromises !== 'undefined' ? maxQueuedPromises : Infinity;
      this.queue = [];
  }

  /**
   * Defines promise promiseFactory
   * @param {Function} GlobalPromise
   */
  Queue.configure = function (GlobalPromise) {
      LocalPromise = GlobalPromise;
  };

  /**
   * @param {Function} promiseGenerator
   * @return {LocalPromise}
   */
  Queue.prototype.push = function (promiseGenerator) {
      var self = this;
      return new LocalPromise(function (resolve, reject, notify) {
          // Do not queue to much promises
          if (self.queue.length >= self.maxQueuedPromises) {
              reject(new Error('Queue limit reached'));
              return;
          }

          // Add to queue
          self.queue.push({
              promiseGenerator: promiseGenerator,
              resolve: resolve,
              reject: reject,
              notify: notify || noop
          });

          self._dequeue();
      });
  };

  /**
   * @param {Function} promiseGenerator
   * @return {LocalPromise}
   */
  Queue.prototype.unshift = function (promiseGenerator) {
    var self = this;
    return new LocalPromise(function (resolve, reject, notify) {
        // Do not queue to much promises
        if (self.queue.length >= self.maxQueuedPromises) {
            reject(new Error('Queue limit reached'));
            return;
        }

        // Add to queue
        self.queue.unshift({
            promiseGenerator: promiseGenerator,
            resolve: resolve,
            reject: reject,
            notify: notify || noop
        });

        self._dequeue();
    });
  };

  /**
   * Number of simultaneously running promises (which are resolving)
   *
   * @return {number}
   */
  Queue.prototype.getPendingLength = function () {
      return this.pendingPromises;
  };

  /**
   * Number of queued promises (which are waiting)
   *
   * @return {number}
   */
  Queue.prototype.getQueueLength = function () {
      return this.queue.length;
  };

  /**
   * @returns {boolean} true if first item removed from queue
   * @private
   */
  Queue.prototype._dequeue = function () {
      var self = this;
      if (this.pendingPromises >= this.maxPendingPromises) {
          return false;
      }

      // Remove from queue
      var item = this.queue.shift();
      if (!item) {
          return false;
      }

      try {
          this.pendingPromises++;

          resolveWith(item.promiseGenerator())
          // Forward all stuff
              .then(function (value) {
                  // It is not pending now
                  self.pendingPromises--;
                  // It should pass values
                  item.resolve(value);
                  self._dequeue();
              }, function (err) {
                  // It is not pending now
                  self.pendingPromises--;
                  // It should not mask errors
                  item.reject(err);
                  self._dequeue();
              }, function (message) {
                  // It should pass notifications
                  item.notify(message);
              });
      } catch (err) {
          self.pendingPromises--;
          item.reject(err);
          self._dequeue();

      }

      return true;
  };

  return Queue;
});